package cn.xw.utils.qrcode;

import com.google.zxing.*;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.geom.RoundRectangle2D;
import java.awt.image.BufferedImage;
import java.io.*;
import java.net.*;
import java.nio.file.Paths;
import java.util.*;

/**
 * 二维码和条形码的生成和解析
 *
 * @author Anhui OuYang
 * @version 1.0
 **/
public class GenerateQRCode {

    private final Logger log = LoggerFactory.getLogger(this.getClass());

    // 生成二维码或条形码的配置信息（正常使用默认即可，无需更改）
    private QRCodeProperties properties = new QRCodeProperties();

    /***
     * 空参构造器
     */
    public GenerateQRCode() {
    }

    /***
     * 单参构造器
     * @param properties 配置信息
     */
    public GenerateQRCode(QRCodeProperties properties) {
        this.properties = properties;
    }

    /***
     * 根据传入的二维码信息转换为具体的二维码内容
     * @param codeFile 二维码文件
     * @return 返回解析的信息
     */
    public String decodeQRCode(File codeFile) {
        if (codeFile == null) {
            log.info("解析的二维码图片为空。");
            throw new RuntimeException("解析的二维码图片为空。");
        }
        //初始化返回信息
        String result = "";
        try {
            result = decodeQRCode(new FileInputStream(codeFile));
        } catch (FileNotFoundException e) {
            log.info("二维码文件读取失败，找不到二维码文件信息：{}", e.getMessage());
            throw new RuntimeException(e);
        }
        return result;
    }

    /***
     * 根据传入的二维码信息转换为具体的二维码内容
     * @param codeInput 二维码流对象
     * @return 返回解析的信息
     */
    public String decodeQRCode(InputStream codeInput) {
        //初始化返回信息
        String result = "";
        try {
            //创建一个图像数据缓冲区类(并读取二维码图片)
            BufferedImage bufferedImage = ImageIO.read(codeInput);
            // 调用解析
            result = decodeQRCode(bufferedImage).getText();
        } catch (IOException e) {
            log.info("二维码解析失败，转换图像数据缓冲对象失败：{}", e.getMessage());
            throw new RuntimeException(e);
        } catch (NotFoundException e) {
            log.info("二维码无法识别：{}", e.getMessage());
            throw new RuntimeException(e);
        }
        return result;
    }

    /***
     * 根据传入的二维码信息转换为具体的二维码内容
     * @param bufferedImage 二维码图像数据缓冲区
     * @return 返回解析的信息对象
     */
    public Result decodeQRCode(BufferedImage bufferedImage) throws NotFoundException {
        //初始化返回信息
        Result result = null;
        // 对图片信息的图像处理和分析
        // 注意需要 zxing 库的版本 3.2.1 或更高版本才能使用，若没有则使用我下面定义的getRGBPixelData(BufferedImage image)
        //BufferedImageLuminanceSource luminanceSource = new BufferedImageLuminanceSource(bufferedImage);
        // 若版本不够则使用这种方式
        RGBLuminanceSource luminanceSource = new RGBLuminanceSource(bufferedImage.getWidth(),
                bufferedImage.getHeight(), getRGBPixelData(bufferedImage));
        // 创建BinaryBitmap对象
        // BinaryBitmap是二值化位图，将像素从RGB空间转换为黑白颜色空间
        BinaryBitmap binaryBitmap = new BinaryBitmap(new HybridBinarizer(luminanceSource));
        // 设置解码参数
        Map<DecodeHintType, Object> hints = new HashMap<>();
        hints.put(DecodeHintType.CHARACTER_SET, properties.getCharset());   // 设置解析的字符集
        // 使用 MultiFormatReader 进行解码
        MultiFormatReader formatReader = new MultiFormatReader();
        formatReader.setHints(hints);                           // 设置解码一些参数
        result = formatReader.decode(binaryBitmap);   // 解码（解析binaryBitmap数据）
        return result;
    }

    /***
     * 生成二维码带中间Logo图片（Logo图片为文件路径）
     * @param content 二维码承载的内容
     * @param logoPath 二维码中间logo图片地址
     * @param response 响应对象
     */
    public void encodeQRCode(String content, String logoPath, HttpServletResponse response) {
        try {
            //初始化File
            File file = buildCodeFile(logoPath);
            encodeQRCode(content, file, response);
        } catch (URISyntaxException e) {
            log.info("创建二维码时，中间Logo图标文件构建失败：{}", logoPath);
            throw new RuntimeException(e);
        }
    }

    /***
     * 生成二维码带中间Logo图片（Logo图片为File文件）
     * @param content 二维码承载的内容
     * @param logoFile 二维码中间logo图片文件
     * @param response 响应对象
     */
    public void encodeQRCode(String content, File logoFile, HttpServletResponse response) {
        if (logoFile == null) {
            log.info("创建二维码时，中间Logo图标文件为空");
            throw new RuntimeException("二维码中间Logo图标文件不存在");
        }
        try {
            encodeQRCode(content, new FileInputStream(logoFile), response);
        } catch (FileNotFoundException e) {
            log.info("文件读取失败，找不到Logo文件信息：{}", e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /***
     * 生成带Logo图标二维码的核心方法，前面几个重载方法都是调用这个
     * @param content 二维码承载的内容
     * @param logoInput 二维码中间logo图片流信息
     * @param response 响应
     */
    public void encodeQRCode(String content, InputStream logoInput, HttpServletResponse response) {
        // 判断若logoInput为则无法构建带Logo的二维码
        if (logoInput == null) {
            log.info("无法构建带Logo的二维码图片，原因Logo图片流信息不存在。");
        }

        try {
            //调用构建二维码
            BufferedImage bufferedImage =
                    encodeCodeImage(content, properties.getCodeWidth(), properties.getCodeHeight(), 0);
            // 添加Logo中间图片信息
            insertLogoImage(bufferedImage, logoInput);
            //判断二维码是否需要被校验（保证生成的二维码是可读的）
            if (properties.getCheckQRCode()) {
                decodeQRCode(bufferedImage);
            }
            // 根据响应对象获取输出流对象
            ServletOutputStream output = response.getOutputStream();
            // properties.getFormatName().getType()获取文件输出的内容，并输出图片
            ImageIO.write(bufferedImage, properties.getFormatName().getType(), output);
        } catch (NotFoundException e) {
            log.info("生成的二维码校验失败，请调大二维码的宽度、高度或调整容错等级。");
            throw new RuntimeException(e);
        } catch (Exception e) {
            log.info("二维码图片生成失败：{}", e.getMessage());
        }
    }

    /***
     * 生成不带Logo图标二维码的核心方法，
     * @param content 二维码承载的内容
     * @param response 响应
     */
    public void encodeQRCode(String content, HttpServletResponse response) {
        try {
            //调用构建二维码
            BufferedImage bufferedImage =
                    encodeCodeImage(content, properties.getCodeWidth(), properties.getCodeHeight(), 0);
            //判断二维码是否需要被校验（保证生成的二维码是可读的）
            if (properties.getCheckQRCode()) {
                decodeQRCode(bufferedImage);
            }
            // 根据响应对象获取输出流对象
            ServletOutputStream output = response.getOutputStream();
            // properties.getFormatName().getType()获取文件输出的内容，并输出图片
            ImageIO.write(bufferedImage, properties.getFormatName().getType(), output);
        } catch (NotFoundException e) {
            log.info("生成的二维码校验失败，请调大二维码的宽度、高度或调整容错等级。");
            throw new RuntimeException(e);
        } catch (Exception e) {
            log.info("二维码图片生成失败：{}", e.getMessage());
        }
    }

    /***
     * 条形码生成并输出
     * @param content 条形码信息EAN_13格式 如：”6971483746515“
     * @param response 响应
     */
    public void encodeBarcode(String content, HttpServletResponse response) {
        try {
            //调用构建二维码
            BufferedImage bufferedImage =
                    encodeCodeImage(content, properties.getBarCodeWidth(), properties.getBarCodeHeight(), 1);
            // 根据响应对象获取输出流对象
            ServletOutputStream output = response.getOutputStream();
            // properties.getFormatName().getType()获取文件输出的内容，并输出图片
            ImageIO.write(bufferedImage, properties.getFormatName().getType(), output);
        } catch (Exception e) {
            log.info("条形码图片生成失败：{}", e.getMessage());
            throw new RuntimeException(e);
        }
    }

    /***
     * 设置一些响应头信息（工具类方法，用来设置生成出来的二维码或条形码是否直接以下载的方式在网页下载）
     * @param response 响应
     */
    public void setResponseHeaderInfoDownload(HttpServletResponse response) {
        //生成二维码文件名称
        String code = UUID.randomUUID().toString().replace("-", "");
        String codeName = code + "." + properties.getFormatName().getType();
        //获取文件名的MIME类型
        //String mimeType = URLConnection.getFileNameMap().getContentTypeFor(codeName);
        // 设置response响应头信息
        response.setContentType(properties.getFormatName().getMineType());
        response.setHeader("Content-Disposition", "attachment;filename=" + codeName);
    }

    /***
     * 根据内容将其写入二维码或者条形码中
     * @param content 生成二维码或者条形码信息
     * @param width 码的长度
     * @param height 码的高度
     * @param type 码的类型 0：二维码  1：条形码
     * @return 生成的条形码或者二维码图片流信息
     * @throws WriterException 图片写入异常
     */
    private BufferedImage encodeCodeImage(String content, int width, int height, Integer type) throws WriterException {
        // 用于写入条形码或二维码
        MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
        BitMatrix bitMatrix = null;
        if (type == 0) {
            bitMatrix = multiFormatWriter
                    .encode(content, QRCodeProperties.qrcodeType, width, height, getEncodeHint());
        }
        if (type == 1) {
            bitMatrix = multiFormatWriter
                    .encode(content, QRCodeProperties.barcodeType, width, height, getEncodeHint());
        }
        // 把条形码信息转换为图像信息
        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_BGR);
        for (int x = 0; x < width; x++) {
            for (int y = 0; y < height; y++) {
                assert bitMatrix != null;
                bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? properties.getFrontColor()
                        : properties.getBackgroundColor());
            }
        }
        return bufferedImage;
    }

    /***
     * 为二维码插入中间的logo图片；注意只能二维码插入图片
     * @param bufferedImage 二维码图片信息
     * @param logoInput logo图片位置
     * @throws IOException  抛出IO异常
     */
    private void insertLogoImage(BufferedImage bufferedImage, InputStream logoInput) throws IOException {
        //判断当前Logo流是否为空或者二维码流是否为空
        if (bufferedImage == null || logoInput == null) {
            log.info("二维码中间加入Logo图标失败，提供流信息不完整。");
            return;
        }
        // 加载Logo图片到Image对象
        Image logoImage = ImageIO.read(logoInput);
        // 设置二维码中间的Logo图片(设置logo图片的宽高需要保持和二维码宽高的一半宽高)
        int logoWidth = properties.getCodeWidth() / 5;
        int logoHeight = properties.getCodeHeight() / 5;
        // 按照指定的宽高进行等比缩放(若设置的宽高比之前大则代表放大)，并将缩放后的图片绘制到一个新的BufferedImage对象中
        Image image = logoImage.getScaledInstance(logoWidth, logoHeight, Image.SCALE_SMOOTH);
        BufferedImage logoBufferedImage = new BufferedImage(logoWidth, logoHeight, BufferedImage.TYPE_INT_RGB);
        // 获取图形上下文对象，用于后续将缩放后的图片绘制在 logoBufferedImage 对象上
        Graphics graphics = logoBufferedImage.getGraphics();
        // 绘制缩放后的图片到 logoBufferedImage 对象上，使其填满整个画布
        graphics.drawImage(image, 0, 0, null);
        // 释放图形上下文对象，避免内存泄漏
        graphics.dispose();
        // 把处理好的logo图片再次设置给之前的logo图片对象
        logoImage = image;

        // 开始把logo图片插入到二维码的中间位置
        // 获取 Graphics2D 对象，用于后续在 bufferedImage 上绘制二维码和 logo 图片
        Graphics2D graph = bufferedImage.createGraphics();
        // 计算出 logo 图片在二维码中间的坐标点 (x, y)，以便后续将 logo 图片插入到正确的位置。
        int x = (properties.getCodeWidth() - logoWidth) / 2;
        int y = (properties.getCodeHeight() - logoHeight) / 2;
        // 绘制缩放后的 logo 图片到二维码中间位置
        graph.drawImage(logoImage, x, y, logoWidth, logoHeight, null);
        // 设置边框的线条粗细为 3 像素，并且设置线条是一个圆角矩形，6表示圆角的半径。并关闭资源
        graph.setStroke(new BasicStroke(3f));
        graph.draw(new RoundRectangle2D.Float(x, y, logoWidth, logoHeight, 6, 6));
        graph.dispose();
    }

    /***
     * 指定和控制二维码生成器的一些参数的类
     * @return 具体的配置信息
     */
    private Map<EncodeHintType, Object> getEncodeHint() {
        Map<EncodeHintType, Object> hints = new HashMap<>();
        hints.put(EncodeHintType.CHARACTER_SET, properties.getCharset());
        hints.put(EncodeHintType.ERROR_CORRECTION, properties.getErrorCorrection());
        hints.put(EncodeHintType.MARGIN, properties.getMargin());
        return hints;
    }

    /***
     * 根据文件的绝对路径或者相对路径（在src/main/resources下的相对路径）来创建File对象
     * @param logoPath 文件路径
     * @return 返回创建的文件信息
     * @throws URISyntaxException   URI异常
     */
    private File buildCodeFile(String logoPath) throws URISyntaxException {
        File file = null;
        // 判断路径是否为空
        if (logoPath != null && !logoPath.isEmpty()) {
            // 判断路径是否为绝对路径
            if (Paths.get(logoPath).isAbsolute()) {
                file = new File(logoPath);
            } else {
                // 获取当前工程的类加载器对象
                ClassLoader classLoader = this.getClass().getClassLoader();
                // 获取资源文件的 URL 对象
                URL url = classLoader.getResource(logoPath);
                // 判断是否能够获取到 URL 对象
                if (url != null) {
                    // 将 URL 转换为 URI 对象 并创建文件对象
                    URI uri = url.toURI();
                    file = new File(uri);
                }
            }
        }
        return file;
    }

    /***
     * 将 BufferedImage 转为 RGB 像素数组
     * @param image 图像数据缓冲区类信息
     * @return 返回RGB像素数组
     */
    private static int[] getRGBPixelData(BufferedImage image) {
        // 获取 BufferedImage 对象的宽度和高度
        int width = image.getWidth();
        int height = image.getHeight();
        // 计算 RGB 像素数组的长度，并创建数组
        int[] pixels = new int[width * height];
        // 获取 BufferedImage 对象的像素数据，并将其存储到 RGB 像素数组中
        image.getRGB(0, 0, width, height, pixels, 0, width);
        // 返回 RGB 像素数组
        return pixels;
    }

    public QRCodeProperties getProperties() {
        return properties;
    }

    public void setProperties(QRCodeProperties properties) {
        this.properties = properties;
    }
}
